package com.tianji.promotion.service.impl;

import cn.hutool.core.bean.copier.CopyOptions;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.common.autoconfigure.mq.RabbitMqHelper;
import com.tianji.common.autoconfigure.redisson.annotations.Lock;
import com.tianji.common.constants.MqConstants;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.BeanUtils;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.constants.PromotionConstants;
import com.tianji.promotion.domain.dto.UserCouponDTO;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.ExchangeCode;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.domain.query.UserCouponQuery;
import com.tianji.promotion.domain.vo.CouponVO;
import com.tianji.promotion.enums.ExchangeCodeStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.service.IUserCouponService;
import com.tianji.promotion.utils.CodeUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * <p>
 * 用户领取优惠券的记录，是真正使用的优惠券信息 服务实现类
 * 分布式锁
 * 异步领券
 * </p>
 *
 * @author huanmo
 * @since 2025-09-27
 */
@RequiredArgsConstructor
public class UserCouponServiceImpl2 extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {

    private final CouponMapper couponMapper;

    private final IExchangeCodeService codeService;

    private final StringRedisTemplate redisTemplate;

    private final RabbitMqHelper mqHelper;

    @Override
    @Lock(name = "lock:coupon:#{couponId}")
    public void receiveCoupon(Long couponId) {
        // 1.查询优惠券
        Coupon coupon = queryCouponByCache(couponId);
        if (coupon == null) {
            throw new BadRequestException("优惠券不存在");
        }
        // 2.校验发放时间
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {
            throw new BadRequestException("优惠券发放已经结束或尚未开始");
        }
        // 3.校验库存
        if (coupon.getTotalNum() <= 0) {
            throw new BadRequestException("优惠券库存不足");
        }
        Long userId = UserContext.getUser();
        // 4.校验每人限领数量
        // 4.1.查询领取数量
        String key = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + couponId;
        Long count = redisTemplate.opsForHash().increment(key, userId.toString(), 1);
        // 4.2.校验限领数量
        if (count > coupon.getUserLimit()) {
            throw new BadRequestException("超出领取数量");
        }
        // 5.扣减优惠券库存
        redisTemplate.opsForHash().increment(
                PromotionConstants.COUPON_CACHE_KEY_PREFIX + couponId, "totalNum", -1);
        // 6.发送MQ消息
        UserCouponDTO uc = new UserCouponDTO();
        uc.setUserId(userId);
        uc.setCouponId(couponId);
        mqHelper.send(MqConstants.Exchange.PROMOTION_EXCHANGE, MqConstants.Key.COUPON_RECEIVE, uc);
    }

    /**
     * 可以实现：不把兑换码写入缓存，也能查询缓存的
     * 不缓存完整的兑换码信息，而是通过序列号映射到优惠券ID
     * <p>
     * 1. 序列号与优惠券的映射关系
     * 在 asyncGenerateCode 方法中
     * redisTemplate.opsForZSet().add(COUPON_RANGE_KEY, coupon.getId().toString(), maxSerialNum);
     * 这里存储的是：
     * Key: coupon:code:range (常量 COUPON_RANGE_KEY)
     * Member: 优惠券ID (如 "123")
     * Score: 该优惠券对应的最大序列号
     * <p>
     * 2. 通过序列号查找优惠券ID
     * public Long exchangeTargetId(long serialNum) {
     *     // 查询score值比当前序列号大的第一个优惠券
     *     Set<String> results = redisTemplate.opsForZSet().rangeByScore(
     *             COUPON_RANGE_KEY, serialNum, serialNum + 5000, 0L, 1L);
     *     // ...
     * }
     * <p>
     * 3. 查询原理
     * 假设有以下数据：
     * ZSet: coupon:code:range
     * Member     Score
     * "100"      500     → 优惠券100的序列号范围: 1-500
     * "200"      1000    → 优惠券200的序列号范围: 501-1000
     * "300"      1500    → 优惠券300的序列号范围: 1001-1500
     * 当查询序列号 750 时：
     * rangeByScore(750, 750 + 5000, 0, 1)
     * 找到第一个 Score ≥ 750 的成员 → "200" (Score=1000)
     * 说明序列号750属于优惠券200
     * <p>
     * 4. 兑换状态管理
     * 使用 BitMap 记录兑换状态
     * boolean exchanged = codeService.updateExchangeMark(serialNum, true);
     * 这里用 Redis BitMap 来记录每个序列号是否被兑换，而不是缓存完整的兑换码信息。
     * <p>
     * 优势
     * 内存高效：只存储映射关系和兑换状态，不存储完整的兑换码对象
     * 查询快速：ZSet 的 rangeByScore 操作时间复杂度 O(logN)
     * 扩展性好：新增优惠券只需添加新的 ZSet 成员
     * <p>
     * 完整流程
     * 生成阶段：为每个优惠券分配连续的序列号范围
     * 兑换阶段：解析兑换码得到序列号；通过序列号找到对应的优惠券ID；检查兑换状态；执行兑换逻辑。
     * 这种设计在保证功能的同时，极大减少了 Redis 的内存使用。
     */
    @Override
    @Lock(name = "'lock:coupon:exchange'")
    public void exchangeCoupon(String code) {
        // 1.校验并解析兑换码
        long serialNum = CodeUtil.parseCode(code);
        // 2.校验是否已经兑换 SETBIT KEY 4 1
        boolean exchanged = codeService.updateExchangeMark(serialNum, true);
        if (exchanged) {
            throw new BizIllegalException("兑换码已经被兑换过了");
        }
        try {
            // 3.查询兑换码对应的优惠券id
            Long couponId = codeService.exchangeTargetId(serialNum);
            if (couponId == null) {
                throw new BizIllegalException("兑换码不存在！");
            }
            Coupon coupon = queryCouponByCache(couponId);
            if (coupon == null) {
                throw new BizIllegalException("缓存中不存在该优惠券");
            }
            // 4.是否过期
            LocalDateTime now = LocalDateTime.now();
            if (now.isAfter(coupon.getIssueEndTime()) || now.isBefore(coupon.getIssueBeginTime())) {
                throw new BizIllegalException("优惠券活动未开始或已经结束");
            }
            // 5.校验每人限领数量
            Long userId = UserContext.getUser();
            // 5.1.查询领取数量
            String key = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + couponId;
            Long count = redisTemplate.opsForHash().increment(key, userId.toString(), 1);
            // 5.2.校验限领数量
            if (count > coupon.getUserLimit()) {
                throw new BadRequestException("超出领取数量");
            }
            // 6.发送MQ消息通知
            UserCouponDTO uc = new UserCouponDTO();
            uc.setUserId(userId);
            uc.setCouponId(couponId);
            uc.setSerialNum((int) serialNum);
            mqHelper.send(MqConstants.Exchange.PROMOTION_EXCHANGE, MqConstants.Key.COUPON_RECEIVE, uc);
        } catch (Exception e) {
            // 重置兑换的标记 0
            codeService.updateExchangeMark(serialNum, false);
            throw e;
        }
    }

    @Override
    public PageDTO<CouponVO> queryMyCouponByPage(UserCouponQuery query) {
        Long userId = UserContext.getUser();
        // 1.查询我的优惠券列表
        Page<UserCoupon> page = lambdaQuery()
                .eq(UserCoupon::getUserId, userId)
                .eq(query.getStatus() != null, UserCoupon::getStatus, query.getStatus())
                .page(query.toMpPageDefaultSortByCreateTimeDesc());
        List<UserCoupon> records = page.getRecords();
        if (CollUtils.isEmpty(records)) {
            return PageDTO.empty(page);
        }
        // 2.查询优惠券
        List<Long> couponIds = records.stream()
                .map(UserCoupon::getCouponId)
                .distinct()
                .collect(Collectors.toList());
        List<Coupon> coupons = couponMapper.selectBatchIds(couponIds);
        if (CollUtils.isEmpty(coupons)) {
            return PageDTO.empty(page);
        }
        // 3.封装VO
        List<CouponVO> list = new ArrayList<>(records.size());
        for (Coupon coupon : coupons) {
            CouponVO vo = BeanUtils.copyProperties(coupon, CouponVO.class);
            list.add(vo);
        }
        return PageDTO.of(page, list);
    }

    @Transactional
    @Override
    public void checkAndCreateUserCoupon(UserCouponDTO uc) {
        // 1.查询优惠券
        Coupon coupon = couponMapper.selectById(uc.getCouponId());
        if (coupon == null) {
            throw new BizIllegalException("优惠券不存在！");
        }
        // 2.更新优惠券的已经发放的数量 + 1
        int r = couponMapper.incrIssueNum(coupon.getId());
        if (r == 0) {
            throw new BizIllegalException("优惠券库存不足！");
        }
        // 3.新增一个用户券
        saveUserCoupon(coupon, uc.getUserId());
        // 4.更新兑换码状态
        if (uc.getSerialNum() != null) {
            codeService.lambdaUpdate()
                    .set(ExchangeCode::getUserId, uc.getUserId())
                    .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                    .eq(ExchangeCode::getId, uc.getSerialNum())
                    .update();
        }
    }

    @Override
    public void writeOffCoupon(List<Long> userCouponIds) {

    }

    @Override
    public void refundCoupon(List<Long> userCouponIds) {

    }

    @Override
    public List<String> queryDiscountRules(List<Long> userCouponIds) {
        return List.of();
    }

    private Coupon queryCouponByCache(Long couponId) {
        // 1.准备KEY
        String key = PromotionConstants.COUPON_CACHE_KEY_PREFIX + couponId;
        // 2.查询
        Map<Object, Object> objMap = redisTemplate.opsForHash().entries(key);
        if (objMap.isEmpty()) {
            return null;
        }
        // 3.数据反序列化
        return BeanUtils.mapToBean(objMap, Coupon.class, false, CopyOptions.create());
    }

    private void saveUserCoupon(Coupon coupon, Long userId) {
        // 1.基本信息
        UserCoupon uc = new UserCoupon();
        uc.setUserId(userId);
        uc.setCouponId(coupon.getId());
        // 2.有效期信息
        LocalDateTime termBeginTime = coupon.getTermBeginTime();
        LocalDateTime termEndTime = coupon.getTermEndTime();
        if (termBeginTime == null) {
            termBeginTime = LocalDateTime.now();
            termEndTime = termBeginTime.plusDays(coupon.getTermDays());
        }
        uc.setTermBeginTime(termBeginTime);
        uc.setTermEndTime(termEndTime);
        // 3.保存
        save(uc);
    }
}
